<?php
class APPL_Db_ActiveRecord_Abstract
{
    const ZEND_DB_ACTIVERECORD_NEW   = 1;
    const ZEND_DB_ACTIVERECORD_DIRTY = 2;
    const ZEND_DB_ACTIVERECORD_CLEAN = 3;
   
    /**
     * @var array
     */
    protected static $_objects = array();

    /**
     * @var array
     */
    protected static $_relationships = array();

    /**
     * @var array
     */
    protected static $_tableInfo = array();   

    /**
     * @var int
     */
    protected $_status;

    /**
     * @var array
     */
    protected $_data = array();

    /**
     * @var array
     */
    protected $_modified = array();

    /**
     * Default Zend_Db_Adapter_Abstract object.
     *
     * @var Zend_Db_Adapter_Abstract
     */
    protected static $_defaultAdapter;

    /**
     * Zend_Db_Adapter_Abstract object.
     *
     * @var Zend_Db_Adapter_Abstract
     */
    protected $_adapter;

    /**
     * Constructor.
     *
     * @return void
     */
    public function __construct($data = array())
    {
        $this->_status = self::ZEND_DB_ACTIVERECORD_NEW;
        $this->_init();
        $this->_data = array_merge($this->_data, $data);
    }

    /**
     * Turnkey for initialization of a table object.
     * Calls other protected methods for individual tasks, to make it easier
     * for a subclass to override part of the setup logic.
     *
     * @return void
     */
    protected function _init()
    {
        if (!array_key_exists(get_class($this), self::$_relationships)) {
            self::$_relationships[get_class($this)] = array();
        }
        if (!array_key_exists(get_class($this), self::$_objects)) {
            self::$_objects[get_class($this)] = array();
        }
        if (!array_key_exists(get_class($this), self::$_tableInfo)) {
            self::$_tableInfo[get_class($this)] = null;
        }
        $this->_setupDatabaseAdapter();
        $this->_setupDataArray();
        $this->_initRelationships();
    }

    /**
     * @return array
     */
    protected function _getTableInfo()
    {
        if (null === self::$_tableInfo[get_class($this)]) {
            self::$_tableInfo[get_class($this)] = $this->getAdapter()->describeTable($this->_getTableName());
        }
        return self::$_tableInfo[get_class($this)];
    }

    /**
     * Initialize database adapter.
     *
     * @return void
     */
    protected function _setupDatabaseAdapter()
    {
        if (!$this->_adapter) {
            $this->_adapter = self::getDefaultAdapter();
            if (!$this->_adapter instanceof Zend_Db_Adapter_Abstract) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception('No adapter found for ' . get_class($this));
            }
        }
    }

    /**
     * Sets the default Zend_Db_Adapter_Abstract for all Zend_Db_Table objects.
     *
     * @param  mixed $adapter Either an Adapter object, or a string naming a Registry key
     * @return void
     */
    public static function setDefaultAdapter($adapter = null)
    {
        self::$_defaultAdapter = self::_setupAdapter($adapter);
    }

    /**
     * Gets the default Zend_Db_Adapter_Abstract for all Zend_Db_Table objects.
     *
     * @return Zend_Db_Adapter_Abstract or null
     */
    public static function getDefaultAdapter()
    {
        return self::$_defaultAdapter;
    }

    /**
     * @param  mixed $adapter Either an Adapter object, or a string naming a Registry key
     * @return APPL_Db_ActiveRecord_Abstract Provides a fluent interface
     */
    protected function _setAdapter($adapter)
    {
        $this->_adapter = self::_setupAdapter($adapter);
        return $this;
    }

    /**
     * Gets the Zend_Db_Adapter_Abstract for this particular APPL_Db_ActiveRecord object.
     *
     * @return Zend_Db_Adapter_Abstract
     */
    public function getAdapter()
    {
        return $this->_adapter;
    }

    /**
     * @param  mixed $adapter Either an Adapter object, or a string naming a Registry key
     * @return Zend_Db_Adapter_Abstract
     * @throws APPL_Db_ActiveRecord_Exception
     */
    protected static function _setupAdapter($adapter)
    {
        if ($adapter === null) {
            return null;
        }
        if (is_string($adapter)) {
            require_once 'Zend/Registry.php';
            $adapter = Zend_Registry::get($adapter);
        }
        if (!$adapter instanceof Zend_Db_Adapter_Abstract) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new APPL_Db_ActiveRecord_Exception(
                'Argument must be of type Zend_Db_Adapter_Abstract, or a Registry '
               .'key where a Zend_Db_Adapter_Abstract object is stored'
            );
        }
        return $adapter;
    }

    public function insert()
    {
        $table = (null != $this->_getSchemaName()) 
               ? $this->_getSchemaName().".".$this->_getTableName()
               : $this->_getTableName();
        $this->getAdapter()->insert($table, $this->_data); 
        $this->_modified = array();
        $self->_status = self::ZEND_DB_ACTIVERECORD_CLEAN;
    }
    public function update()
    {
        $table = (null != $this->_getSchemaName()) 
               ? $this->_getSchemaName().".".$this->_getTableName()
               : $this->_getTableName();
        $this->getAdapter()->update($table, $this->_modified, $this->_getPrimaryWhere()); 
        $this->_modified = array();
        $self->_status = self::ZEND_DB_ACTIVERECORD_CLEAN;
    }

    public function save()
    {
        if ($this->_status == self::ZEND_DB_ACTIVERECORD_DIRTY) {
            $this->update();
        } elseif ($this->_status == self::ZEND_DB_ACTIVERECORD_NEW) {
            $this->insert();
        }
        foreach (self::$_objects[get_class($this)] as $property=>$mixed) {
            if ($mixed instanceof APPL_Db_ActiveRecord_Abstract) {
                $mixed->save();
            } elseif ($mixed instanceof ArrayObject) {
                foreach ($mixed as $object) {
                    $object->save();
                }
            }
        }
    }

    public function delete()
    {
        $where = $this->_getPrimaryWhere();
        $this->getAdapter()->delete($this->_getSchemaName().".".$this->_getTableName(), $where); 
    }

    public static function create($data = array())
    {
        $called = get_called_class();
        return new $called($data);
    }

    public static function find()
    {
        $self = self::create();
        $self->getAdapter()->setFetchMode(Zend_Db::FETCH_ASSOC);
        if (func_num_args() == 1 && is_array(func_get_arg(0))) {
            $select = self::select();
            $adapter = $self->getAdapter();
            $table = $self->_getTableName();
            $info = $self->_getTableInfo();
            $ct = 0;
            $tableName = $adapter->quoteIdentifier($table, true);
            foreach (array_keys(func_get_arg(0)) as $column) {
                $columnName = $adapter->quoteIdentifier($column, true);
                $bind = null;
                if ($adapter->supportsParameters('positional')) {
                    $bind = '?';
                } elseif ($adapter->supportsParameters('named')) {
                    $bind = ':'.$ct++;
                } 
                $select->where("$tableName.$columnName = $bind");
            }
            $values = array_values(func_get_arg(0));
        } else {
            $select = $self->_getFindQuery();
            $values = func_get_args();
        }
        return $self->_findOne($select, $values);
    }

    public static function findMany()
    {
        $self = self::create();
        $self->getAdapter()->setFetchMode(Zend_Db::FETCH_ASSOC);
        if (func_num_args() == 1 && is_array(func_get_arg(0))) {
            $select = self::select();
            $adapter = $self->getAdapter();
            $table = $self->_getTableName();
            $info = $self->_getTableInfo();
            $ct = 0;
            $tableName = $adapter->quoteIdentifier($table, true);
            foreach (array_keys(func_get_arg(0)) as $column) {
                $columnName = $adapter->quoteIdentifier($column, true);
                $bind = null;
                if ($adapter->supportsParameters('positional')) {
                    $bind = '?';
                } elseif ($adapter->supportsParameters('named')) {
                    $bind = ':'.$ct++;
                } 
                $select->where("$tableName.$columnName = $bind");
            }
            $values = array_values(func_get_arg(0));
        } else {
            $select = $self::select();
            $values = array();
        }
        return $self->_findMany($select, $values);
    }


    private function _query($select, $values)
    {
        return $this->getAdapter()->fetchAll($select, $values);
    }

    protected function _findMany($select, $values)
    {
        $collection = new ArrayObject();
        $class = get_called_class();
        foreach ($this->_query($select, $values) as $row) {
           $collection->append(new $class($row));
        }
        return $collection; 
    }

    protected function _findOne($select, $values)
    {
        $rowSet = $this->_query($select, $values);
        if (count($rowSet)) {
            $row = array_pop($rowSet);
            $this->_data = array_merge($this->_data, $row);
            $this->_status = self::ZEND_DB_ACTIVERECORD_CLEAN;
        } else {
            $this->_status = self::ZEND_DB_ACTIVERECORD_NEW;
        }
        return $this;
    }
   
    protected function _initRelationships() 
    { 
        // intentionally left blank
    }

    protected function _getTableName()
    {
        return get_class($this);
    }

    protected function _getSchemaName()
    {
        $info = $this->_getTableInfo();
        $details = array_pop($info);
        return $details['SCHEMA_NAME'];
    }

    protected function _setupDataArray()
    {
        $data = array_flip(array_keys($this->_getTableInfo()));
        array_walk($data, create_function('&$v,$k', '$v=null;'));
        $this->_data = $data;
    }
 
    public static function select()
    {
        $self = self::create();
        $table = $self->_getTableName();
        $schema = $self->_getSchemaName();
        $table = (null != $schema) ? "$schema.$table" : $table;
        return $self->getAdapter()->select()->from($table, array_keys($self->_getTableInfo()));
    }

    /**
     * Constructs where statement for retrieving row(s).
     *
     * @return Zend_Db_Select
     */
    protected function _getFindQuery()
    {
        $select = self::select();
        $adapter = $this->getAdapter();
        $table = $this->_getTableName();
        $info = $this->_getTableInfo();
        $ct = 0;
        foreach ($info as $column => $details) {
            if ($details['PRIMARY']) {
                $tableName = $adapter->quoteIdentifier($table, true);
                $columnName = $adapter->quoteIdentifier($column, true);
                $bind = null;
                if ($adapter->supportsParameters('positional')) {
                    $bind = '?';
                } elseif ($adapter->supportsParameters('named')) {
                    $bind = ':'.$ct++;
                } 
                $select->where("$tableName.$columnName = $bind");
            }
        }
        return $select;
    }

    protected function _getPrimaryWhere()
    {
        $where = array();
        $adapter = $this->getAdapter();
        $table = $this->_getTableName();
        $info = $this->_getTableInfo();
        $ct = 0;
        foreach ($info as $column => $details) {
            if ($details['PRIMARY']) {
                $columnName = $adapter->quoteIdentifier($column, true);
                $where["$columnName = ?"] = $this->__get($column);
            }
        }
        return $where;
    }

    /**
     * Transform a column name from the user-specified form
     * to the physical form used in the database.
     * You can override this method in a custom Row class
     * to implement column name mappings, for example inflection.
     *
     * @param string $columnName Column name given.
     * @return string The column name after transformation applied (none by default).
     * @throws Zend_Db_Table_Row_Exception if the $columnName is not a string.
     */
    protected function _transformColumn($columnName)
    {
        if (!is_string($columnName)) {
            require_once 'Zend/Db/Table/Row/Exception.php';
            throw new Zend_Db_Table_Row_Exception('Specified column is not a string');
        }
        // Perform no transformation by default
        return $columnName;
    }

    /**
     * Retrieve row field value
     *
     * @param  string $columnName The user-specified column name.
     * @return string             The corresponding column value.
     * @throws Zend_Db_Table_Row_Exception if the $columnName is not a column in the row.
     */
    public function __get($columnName)
    {
        if (array_key_exists($columnName, self::$_relationships[get_class($this)])) {
            if (!array_key_exists($columnName, self::$_objects[get_class($this)])) {
                $callback = self::$_relationships[get_class($this)][$columnName]['class'].'::'
                          . self::$_relationships[get_class($this)][$columnName]['func'];
                foreach (self::$_relationships[get_class($this)][$columnName]['keys'] as $keys) {
                    $values[$keys['foreign']] = $this->__get($keys['local']);
                }
                self::$_objects[get_class($this)][$columnName] = call_user_func_array($callback, array($values));
            }
            return self::$_objects[get_class($this)][$columnName];
        }
        $columnName = $this->_transformColumn($columnName);
        if (array_key_exists($columnName, $this->_data)) {
            return $this->_data[$columnName];
        }
        require_once 'Zend/Db/Table/Row/Exception.php';
        throw new Zend_Db_Table_Row_Exception("Specified column \"$columnName\" is not in the row");
    }

    /**
     * Set row field value
     *
     * @param  string $columnName The column key.
     * @param  mixed  $value      The value for the property.
     * @return void
     * @throws Zend_Db_Table_Row_Exception
     */
    public function __set($columnName, $value)
    {
        $columnName = $this->_transformColumn($columnName);
        if (array_key_exists($columnName, self::$_relationships[get_class($this)])) {
            if (array_key_exists($columnName, self::$_objects[get_class($this)])) {
                self::$_objects[get_class($this)][$columnName] = $value;
            }
        }
        if (array_key_exists($columnName, $this->_data)) {
            $this->_status = self::ZEND_DB_ACTIVERECORD_DIRTY;
            $this->_modified[$columnName] = $value;
            $this->_data[$columnName] = $value;
            return;
        }
        require_once 'Zend/Db/Table/Row/Exception.php';
        throw new Zend_Db_Table_Row_Exception("Specified column \"$columnName\" is not in the row");
    }

    public static function autoload($class)
    {
       $php = "class $class extends APPL_Db_ActiveRecord_Abstract {}";
       eval($php);
       return true;
    }

    protected static function _addRelationShip($class, $keys, $function)
    {
        $called = get_called_class();
        if (substr_count($class, ' as ')) {
            list($class, $property) = explode(' as ', $class);
        } else { 
            $property = $class;
        }
        $keys = (array_sum(array_keys($keys))) ? $keys : array($keys);
        self::$_relationships[$called][$property] = array(
            'func' =>$function,
            'class' => $class,
            'keys'=>$keys,
        );
    }

    public static function hasOne($class, $keys)
    {
        self::_addRelationship($class, $keys, 'find');
    }

    public static function hasMany($class, $keys)
    {
        self::_addRelationship($class, $keys, 'findMany');
    }
}
